package no.doomsday.scripting.simplescriptengine 
{
	import flash.events.Event;
	import flash.events.EventDispatcher;
	import flash.events.IOErrorEvent;
	import flash.events.TimerEvent;
	import flash.net.URLLoader;
	import flash.net.URLRequest;
	import flash.utils.Dictionary;
	import flash.utils.Timer;
	import no.doomsday.scripting.simplescriptengine.commands.ScriptCommand;
	import no.doomsday.scripting.simplescriptengine.errors.ScriptError;
	import no.doomsday.scripting.simplescriptengine.interfaces.IScriptExecutor;
	import no.doomsday.scripting.simplescriptengine.scriptevents.IScriptBranch;
	import no.doomsday.scripting.simplescriptengine.scriptevents.IScriptExecutable;
	import no.doomsday.scripting.simplescriptengine.scriptevents.ScriptBranchFactory;
	import no.doomsday.scripting.simplescriptengine.scriptevents.ScriptEventFactory;
	/**
	 * ...
	 * @author Andreas Rønning (andreas@doomsday.no)
	 */
	public class ScriptCore implements IScriptExecutor
	{
		private var _callbackMap:Dictionary = new Dictionary();
		private var _branches:Vector.<IScriptBranch>;
		private var _sequence:Vector.<IScriptExecutable>;
		private var _delayTimer:Timer = new Timer(1, 1);
		private var _urlLoader:URLLoader;
		private var _linesExecuted:int;
		private var _returnBuffer:* = null;
		private var _waiting:Boolean = false;
		public var exposedObjects:Dictionary = new Dictionary();
		public var debugMode:Boolean = true;
		public function ScriptCore() 
		{
			print("\tScripting initialized");
			mapCommand("trace", trace);
			mapCommand("delay", delay);
			mapCommand("wait", wait);
			mapCommand("goto", goto);
			mapCommand("if", conditional);
			_urlLoader = new URLLoader();
			_urlLoader.addEventListener(Event.COMPLETE, onScriptLoaded);
			_urlLoader.addEventListener(IOErrorEvent.IO_ERROR, onIOError);
			_delayTimer.addEventListener(TimerEvent.TIMER_COMPLETE, onDelayComplete);
		}
		
		private function conditional(arg:String):Boolean
		{
			trace("Conditional for: " + arg);
			return true;
		}
		public function print(...args:Array):void {
			if (debugMode) trace.apply(this, args);
		}
		//{ gears and cogs and stuff
		private function onIOError(e:IOErrorEvent):void 
		{
			print("\tScript could not be loaded");
		}
		
		private function onScriptLoaded(e:Event):void 
		{
			print("\tScript loaded, executing...");
			run(_urlLoader.data);
		}
		
		private function onDelayComplete(e:TimerEvent):void 
		{
			step();
		}
		public function mapCommand(invoker:String, callback:Function):void {
			if (_callbackMap[invoker] != null) ScriptCommand(_callbackMap[invoker]).dispose();
			_callbackMap[invoker] = new ScriptCommand(invoker, callback);
		}
		private function compile(script:String):void {
			//"COMPILE" hahahaha
			_branches = ScriptBranchFactory.getBranches(script,this);
			if(_branches.length>0){
				_sequence = _branches[0].sequence;
			}
		}
		public function run(script:String):void {
			print("\tExecuting script");
			_linesExecuted = 0;
			try {
				compile(script);
				begin();
			}catch (scriptError:ScriptError) {
				print("\tScript error: " + scriptError.message);
			}catch (e:Error) {
				print("\t" + e.getStackTrace());
			}
		}
		private function begin():void
		{
			step();
		}
		/**
		 * Step to and execute the next script event, if there is one
		 */
		private function step():void {
			if (_delayTimer.running) return;
			if (_linesExecuted <= _sequence.length-1) {
				var success:Boolean = false;
				try {
					var evt:IScriptExecutable = _sequence[_linesExecuted];
					executeEvent(evt);
					success = true;
				}catch (scriptError:ScriptError) {
					print("\tScript error on line "+_linesExecuted+": " + scriptError.message);
				}catch (e:Error) {
					print("\t" + e.getStackTrace());
				}
				if (!success) {
					finish();
				}
			}else {
				finish();
			}
		}
		
		private function executeEvent(event:IScriptExecutable):void
		{
			_linesExecuted++;
			event.execute(this);
		}
		
		private function finish():void
		{
			print("\tScript complete");
		}
		
		public function loadAndRun(url:String):void {
			print("\tLoading script from url " + url);
			_urlLoader.load(new URLRequest(url));
		}
		
		public function tryCommand(invoker:String, args:Array):*
		{
			if (_callbackMap[invoker] != null) {
				var f:Function = _callbackMap[invoker].callback;
				try{
					_returnBuffer = f.apply(this, args);
					if(!_waiting) resume();
					return _returnBuffer;
				}catch (e:Error) {
					throw(new ScriptError(e.message + "\rCallback '" + invoker + "' could not be called with args '" + args + "'"));
				}
			}
			throw(new ScriptError("No callback mapped to invoker '" + invoker + "'"));
		}
		//}
		
		//{ Built in commands
		
		/**
		 * Wait for x milliseconds before stepping to the next event
		 * @param	ms
		 */
		private function delay(ms:int):void {
			print("\tDelaying for " + ms + "ms");
			_delayTimer.delay = ms;
			_delayTimer.reset();
			_delayTimer.start();
		}
		
		/**
		 * Simply stop here, until something calls resume()
		 */
		private function wait():void
		{
			_waiting = true;
		}
		
		/**
		 * Called externally to resume the script after it has been held with wait()
		 */
		public function resume():void {
			_waiting = false;
			step();
		}
		
		/**
		 * Make the script step to a certain script line/command in the squence
		 * @param	line 
		 * int, 1 index
		 */
		private function goto(lineOrBranch:*):void
		{
			if (!isNaN(Number(lineOrBranch))) {
				_linesExecuted = Math.max(0, Math.min(_sequence.length, lineOrBranch-1));
				print("\tStepping to line " + (_linesExecuted+1));
			}else if (lineOrBranch is String) {
				print("Switching branch to " + lineOrBranch);
				switchBranch(lineOrBranch);
			}
		}
		
		/* INTERFACE no.doomsday.scripting.simplescriptengine.interfaces.IScriptExecutor */
		
		public function switchBranch(branchname:String):void
		{
			for (var i:int = 0; i < _branches.length; i++) 
			{
				if (_branches[i].id == branchname) {
					_sequence = _branches[i].sequence;
					goto(1);
					return;
				}
			}
			throw new ScriptError("No branch with name '" + branchname + "'");
		}
		//}
		
		
		
		public function hasCommand(invoker:String):Boolean
		{
			return _callbackMap[invoker] != null;
		}
		
		public function expose(obj:Object):void
		{
			exposedObjects[obj] = obj;
		}
		
		public function get lastReturnValue():* { return _returnBuffer; }
	}

}